﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Files;
using Microsoft.Synchronization.MetadataStorage;
using Microsoft.Synchronization.SimpleProviders;

namespace Microsoft.WindowsAzure.Accelerator.Synchronization
{
    /// <summary>
    /// Azure blob storage provider (for replicas that do not support any type of change tracking). Constructs 
    /// a data store by providing a name for the endpoint (replica) and a file to which we'll persist the sync
    /// metadata (file).
    /// </summary>
    internal class CloudSyncProvider : FullEnumerationSimpleSyncProvider
    {
        private readonly SyncIdFormatGroup _idFormats;
        private readonly String _name;
        private readonly String _replicaIdFile;
        private readonly String _replicaMetadataFile;
        private readonly CloudSyncStore _store;
        private String _metadataDirectory = "";
        private SqlMetadataStore _metadataStore;
        private SyncId _replicaId;

        /// <summary>
        /// Initializes a new instance of the <see cref="CloudSyncProvider"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="store">The store.</param>
        internal CloudSyncProvider(String name, CloudSyncStore store)
        {
            _name = name;
            _store = store;
            _replicaMetadataFile = String.Format(@"{0}\{1}.metadata", MetadataDirectory, _name);
            _replicaIdFile = String.Format(@"{0}\{1}.replicaid", MetadataDirectory, _name);

            //i| Set ItemIdFormat and ReplicaIdFormat for using Guid ids.
            _idFormats = new SyncIdFormatGroup
            {
                ChangeUnitIdFormat =
                {
                    IsVariableLength = false,
                    Length = 4
                },
                ItemIdFormat =
                {
                    IsVariableLength = false,
                    Length = 24
                },
                ReplicaIdFormat =
                {
                    IsVariableLength = false,
                    Length = 16
                }
            };
        }

        /// <summary>
        /// Gets the replica id.
        /// </summary>
        /// <remarks>
        /// Occurs when a file change is about to be tried.
        /// </remarks>
        public SyncId ReplicaId
        {
            get { return _replicaId ?? (_replicaId = GetReplicaIdFromFile(_replicaIdFile)); }
        }

        /// <summary>
        /// Gets a <see cref="T:Microsoft.Synchronization.SyncIdFormatGroup"/> object that represents the format of replica and item IDs.
        /// </summary>
        /// <value></value>
        /// <returns>A <see cref="T:Microsoft.Synchronization.SyncIdFormatGroup"/> object that is used to define the format of replica and item IDs.</returns>
        public override SyncIdFormatGroup IdFormats
        {
            get { return _idFormats; }
        }
        
        /// <summary>
        /// Gets the metadata directory.
        /// </summary>
        private String MetadataDirectory
        {
            get
            {
                if (String.IsNullOrEmpty(_metadataDirectory))
                {
                    //b| Verify that this is filtered from the sync enumeration.
                    _metadataDirectory = Path.GetFullPath(String.Format(@"{0}\cloudsync", (Environment.GetEnvironmentVariable("DiagnosticsLogs") ?? ".")));
                    Trace.TraceInformation("CloudSync | Metadata Directory:  [{0}]", _metadataDirectory);
                    Directory.CreateDirectory(_metadataDirectory);
                }
                return _metadataDirectory;
            }
        }

        /// <summary>
        /// Gets the data store.
        /// </summary>
        internal CloudSyncStore CloudStore
        {
            get { return _store; }
        }

        /// <summary>
        /// Gets the version number for the simple provider.
        /// </summary>
        public override Int16 ProviderVersion
        {
            get { return 2; }
        }

        /// <summary>
        /// Gets an <see cref="T:Microsoft.Synchronization.SimpleProviders.ItemMetadataSchema"/> object that represents the metadata schema for an item.
        /// </summary>
        public override ItemMetadataSchema MetadataSchema
        {
            get
            {
                return new ItemMetadataSchema(
                    new CustomFieldDefinition[2]
                        {
                            new CustomFieldDefinition(CloudSyncItemFields.Name, typeof (String), CloudSyncStore.MaxFileNameLength),
                            new CustomFieldDefinition(CloudSyncItemFields.Timestamp, typeof (UInt64))
                        },
                    new IdentityRule[1]
                        {
                            new IdentityRule(new[]
                                {
                                    CloudSyncItemFields.Name
                                })
                        }
                    );
            }
        }

        /// <summary>
        /// Occurs when applying blob changes.
        /// </summary>
        public event EventHandler<CloudSyncEventArgs> ApplyingChange;

        /// <summary>
        /// Called by the Sync Framework runtime at the start of each session.
        /// </summary>
        public override void BeginSession()
        {
        }

        /// <summary>
        /// Gets the object used to store synchronization metadata.
        /// </summary>
        /// <param name="replicaId">A <see cref="T:Microsoft.Synchronization.SyncId"/> object that contains the ID of the replica for which the <see cref="T:Microsoft.Synchronization.MetadataStorage.MetadataStore"/> object is returned.</param>
        /// <param name="culture">A <see cref="T:System.Globalization.CultureInfo"/> object that represents the culture that is used for string comparisons.</param>
        /// <returns>
        /// A <see cref="T:Microsoft.Synchronization.MetadataStorage.MetadataStore"/> object that represents a metadata store for the specified replica.
        /// </returns>
        /// <remarks>
        /// Currently using the built in sync framework MetadataStore.
        /// </remarks>
        public override MetadataStore GetMetadataStore(out SyncId replicaId, out CultureInfo culture)
        {
            InitializeMetadataStore();
            replicaId = ReplicaId;
            culture = CultureInfo.CurrentCulture;
            return _metadataStore;
        }

        /// <summary>
        /// Gets the list of changes to files in the data store.
        /// </summary>
        /// <remarks>
        /// Enumerates the items currently in the store and returns the properties that were specified by the MetadataSchema property.
        /// </remarks>
        /// <param name="context"></param>
        public override void EnumerateItems(FullEnumerationContext context)
        {
            var items = CloudStore.ListBlobs();
            context.ReportItems(items);
        }

        /// <summary>
        /// Loads data to provide to the other endpoint.
        /// </summary>
        /// <param name="keyAndExpectedVersion">The key and expected version properties of the item for which data is loaded. The provider must perform an optimistic concurrency check to verify that the version of the item on the destination corresponds to the values found in keyAndExpectedVersion. If this check fails, provider should report a recoverable error by using a <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object.</param>
        /// <param name="changeUnitsToLoad">A <see cref="T:Microsoft.Synchronization.SyncId"/> object that contains the change units to load for an item. The parameter should be null (not empty) if no change units are specified.</param>
        /// <param name="recoverableErrorReportingContext">A <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object that is used to report recoverable errors that occur during attempts to update an item.</param>
        /// <returns>
        /// An object that represents the change data for the item with the specified key.
        /// </returns>
        public override Object LoadChangeData(ItemFieldDictionary keyAndExpectedVersion, IEnumerable<SyncId> changeUnitsToLoad, RecoverableErrorReportingContext recoverableErrorReportingContext)
        {
            var expectedAttributes = keyAndExpectedVersion.ToCloudSyncAttributes();
            try
            {
                return CloudStore.GetFileRetriever(expectedAttributes);
            }
            catch (ApplicationException e)
            {
                recoverableErrorReportingContext.RecordRecoverableErrorForChange(new RecoverableErrorData(e));
            }
            return null;
        }

        /// <summary>
        /// Called by the framework to create a file.
        /// </summary>
        /// <param name="itemData">Data for the item in provider-specific format.</param>
        /// <param name="changeUnitsToCreate">A <see cref="T:Microsoft.Synchronization.SyncId"/> object that contains the change units to insert for an item. The parameter should be null (not empty) if no change units are specified.</param>
        /// <param name="recoverableErrorReportingContext">A <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object that is used to report recoverable errors that occur during attempts to insert an item.</param>
        /// <param name="keyAndUpdatedVersion">Returns the key and updated version properties of the item to be inserted. If the return value is not valid, the Sync Framework runtime throws <see cref="T:System.ArgumentOutOfRangeException"/>, which ends the session.</param>
        /// <param name="commitKnowledgeAfterThisItem">Returns whether the Sync Framework runtime should commit knowledge to the metadata store after processing is complete for the specified item.</param>
        public override void InsertItem(Object itemData, IEnumerable<SyncId> changeUnitsToCreate, RecoverableErrorReportingContext recoverableErrorReportingContext, out ItemFieldDictionary keyAndUpdatedVersion, out Boolean commitKnowledgeAfterThisItem)
        {
            var dataRetriever = (IFileDataRetriever)itemData;
            commitKnowledgeAfterThisItem = false;
            keyAndUpdatedVersion = null;
            var attributes = new CloudSyncAttributes(Path.Combine(dataRetriever.RelativeDirectoryPath, dataRetriever.FileData.Name), dataRetriever.FileData.LastWriteTime.ToUniversalTime(), null);
            CloudSyncEventArgs args = new CloudSyncEventArgs(ChangeType.Create, dataRetriever.FileData.RelativePath);
            ApplyingChange(this, args);
            try
            {
                Stream dataStream = null;
                if (!dataRetriever.FileData.IsDirectory)
                {
                    dataStream = dataRetriever.FileStream;
                }
                keyAndUpdatedVersion = CloudStore.InsertFile(dataRetriever.FileData, attributes.Name, dataStream).ToItemFieldDictionary();
            }
            catch (ApplicationException e)
            {
                Trace.TraceError("{0}", e.Message);
                recoverableErrorReportingContext.RecordConstraintError(attributes.ToItemFieldDictionary());
            }
        }

        /// <summary>
        /// Called by the framework to update a file.
        /// </summary>
        /// <param name="itemData">Data for the item in provider-specific format.</param>
        /// <param name="changeUnitsToUpdate">A <see cref="T:Microsoft.Synchronization.SyncId"/> object that contains the change units to update for an item. The parameter should be null (not empty) if no change units are specified.</param>
        /// <param name="keyAndExpectedVersion">The key and expected version properties of the item to be updated. The provider must perform an optimistic concurrency check to verify that the version of the item on the destination corresponds to the values found in keyAndExpectedVersion. If this check fails, provider should report a recoverable error by using a <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object.</param>
        /// <param name="recoverableErrorReportingContext">A <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object that is used to report recoverable errors that occur during attempts to update an item.</param>
        /// <param name="keyAndUpdatedVersion">Returns the key and updated version properties of the updated items. If the return value is not valid, the Sync Framework runtime throws <see cref="T:System.ArgumentOutOfRangeException"/>, which ends the session.</param>
        /// <param name="commitKnowledgeAfterThisItem">Returns whether the Sync Framework runtime should commit knowledge to the metadata store after processing is complete for the specified item.</param>
        public override void UpdateItem(
            Object                           itemData, 
            IEnumerable<SyncId>              changeUnitsToUpdate, 
            ItemFieldDictionary              keyAndExpectedVersion, 
            RecoverableErrorReportingContext recoverableErrorReportingContext, 
            out ItemFieldDictionary          keyAndUpdatedVersion, 
            out Boolean                      commitKnowledgeAfterThisItem)
        {
            var dataRetriever = (IFileDataRetriever)itemData;
            commitKnowledgeAfterThisItem = false;
            keyAndUpdatedVersion = null;
            CloudSyncAttributes updatedAttributes;
            CloudSyncAttributes expectedAttributes = keyAndExpectedVersion.ToCloudSyncAttributes();
            try
            {
                var dataStream = !dataRetriever.FileData.IsDirectory ? dataRetriever.FileStream : null;
                if (IsMoveOrRename(expectedAttributes.Name, dataRetriever.FileData.RelativePath))
                {
                    var args = new CloudSyncEventArgs(ChangeType.Rename, expectedAttributes.Name, dataRetriever.FileData.RelativePath);
                    ApplyingChange(this, args);

                    //i|
                    //i| Moves and renames are treated as delete and create.
                    //i|
                    CloudStore.DeleteFile(expectedAttributes);
                    updatedAttributes = CloudStore.InsertFile(dataRetriever.FileData, dataRetriever.RelativeDirectoryPath, dataStream);
                }
                else
                {
                    var args = new CloudSyncEventArgs(ChangeType.Update, dataRetriever.FileData.Name);
                    ApplyingChange(this, args);
                    updatedAttributes = CloudStore.UpdateFile(expectedAttributes, dataRetriever.FileData, dataRetriever.RelativeDirectoryPath, dataStream);
                }
                
                //i|
                //i| Record information after update.
                //i|
                keyAndUpdatedVersion = updatedAttributes.ToItemFieldDictionary();
            }
            catch (ApplicationException e)
            {
                recoverableErrorReportingContext.RecordRecoverableErrorForChange(new RecoverableErrorData(e));
            }
        }

        /// <summary>
        /// Called by the Sync Framework runtime to delete an item from the destination store.
        /// </summary>
        /// <param name="keyAndExpectedVersion">The key and expected version properties of the item to be deleted. The provider must perform an optimistic concurrency check to verify that the version of the item on the destination corresponds to the values found in keyAndExpectedVersion. If this check fails, provider should report a recoverable error by using a <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object.</param>
        /// <param name="recoverableErrorReportingContext">A <see cref="T:Microsoft.Synchronization.SimpleProviders.RecoverableErrorReportingContext"/> object that is used to report recoverable errors that occur during attempts to delete an item.</param>
        /// <param name="commitKnowledgeAfterThisItem">Returns whether the Sync Framework runtime should commit knowledge to the metadata store after processing is complete for the specified item.</param>
        public override void DeleteItem(
            ItemFieldDictionary              keyAndExpectedVersion, 
            RecoverableErrorReportingContext recoverableErrorReportingContext, 
            out Boolean                      commitKnowledgeAfterThisItem)
        {
            commitKnowledgeAfterThisItem = false;
            var expectedAttributes = keyAndExpectedVersion.ToCloudSyncAttributes();
            var args = new CloudSyncEventArgs(ChangeType.Delete, expectedAttributes.Name);
            ApplyingChange(this, args);
            try
            {
                CloudStore.DeleteFile(expectedAttributes);
            }
            catch (ApplicationException e)
            {
                recoverableErrorReportingContext.RecordRecoverableErrorForChange(new RecoverableErrorData(e));
            }
        }

        /// <summary>
        /// Performs cleanup after the sync operation is completed.
        /// </summary>
        public override void EndSession()
        {
            CloseMetadataStore();
        }

        /// <summary>
        /// Determines whether [is move or rename] [the specified old name].
        /// </summary>
        /// <param name="oldName">The old name.</param>
        /// <param name="newName">The new name.</param>
        /// <returns>
        /// 	<c>true</c> if [is move or rename] [the specified old name]; otherwise, <c>false</c>.
        /// </returns>
        private static Boolean IsMoveOrRename(String oldName, String newName)
        {
            Trace.TraceInformation(@"Rename Test:  ""{0}"" -?-> ""{1}""", oldName, newName);
            return oldName != newName;
        }

#region | METADATA STORAGE

        /// <summary>
        /// Creates or opens the metadata store.
        /// </summary>
        /// <remarks>
        /// Initializes a new store with the ID formats used to reference files and endpoints.
        /// </remarks>
        private void InitializeMetadataStore()
        {
            SyncId id = ReplicaId;
            _metadataStore = !File.Exists(_replicaMetadataFile) 
                ? SqlMetadataStore.CreateStore(_replicaMetadataFile) 
                : SqlMetadataStore.OpenStore(_replicaMetadataFile);
        }

        /// <summary>
        /// Closes the metadata store.
        /// </summary>
        private void CloseMetadataStore()
        {
            _metadataStore.Dispose();
            _metadataStore = null;
        }

        /// <summary>
        /// Cleanup of deleted files older than the provided timespan.
        /// </summary>
        /// <param name="timespan">The timespan for retaining deleted item information.</param>
        internal void CleanupDeletedItems(TimeSpan timespan)
        {
            InitializeMetadataStore();
            SimpleSyncServices simpleSyncServices = new SimpleSyncServices(_idFormats, _metadataStore, ReplicaId, CultureInfo.CurrentCulture, 0);
            simpleSyncServices.CleanupDeletedItems(timespan);
            CloseMetadataStore();
        }

#endregion
#region REPLICA SYNC ID

        /// <summary>
        /// Gets the replica ID from file; creating the replica ID and persistence file the first time requested.
        /// </summary>
        /// <param name="replicaIdFile">Source file for replica ID.</param>
        /// <returns>Replica Sync ID.</returns>
        private static SyncId GetReplicaIdFromFile(String replicaIdFile)
        {
            SyncId replicaId;
            if (File.Exists(replicaIdFile))
            {
                replicaId = ReadReplicaIdFromFile(replicaIdFile);
            }
            else
            {
                replicaId = new SyncId(Guid.NewGuid());
                WriteReplicaIdToFile(replicaIdFile, replicaId);
            }

            return replicaId;
        }

        /// <summary>
        /// Serializes the replica information to a file.
        /// </summary>
        /// <param name="file">Target file for replication information.</param>
        /// <param name="replicaId">Replica SyncId.</param>
        private static void WriteReplicaIdToFile(String file, SyncId replicaId)
        {
            using (FileStream fs = new FileStream(file, FileMode.Create))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                try
                {
                    formatter.Serialize(fs, replicaId);
                }
                catch (SerializationException e)
                {
                    Trace.TraceError("Failed to serialize replica id to file. Reason: " + e.Message);
                    throw;
                }             
            }
        }

        /// <summary>
        /// Deserializes the replica information from the provided file.
        /// </summary>
        /// <param name="file">The source file for replica data.</param>
        /// <returns>SyncId</returns>
        private static SyncId ReadReplicaIdFromFile(String file)
        {
            using (FileStream fs = new FileStream(file, FileMode.Open))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                try
                {
                    return (SyncId) formatter.Deserialize(fs);
                }
                catch (SerializationException e)
                {
                    Trace.TraceError("Failed to deserialize replica id from file. Reason: " + e.Message);
                    throw;
                }
            }
        }

#endregion
    }
}